package com.puzzlebazar.client.puzzle.model;

import java.util.Vector;

import com.google.gwt.user.client.Window;
import com.puzzlebazar.client.services.PuzzleServiceAsync;
import com.puzzlebazar.client.services.ServiceFactory;
import com.puzzlebazar.client.util.AsyncCallbackLock;
import com.puzzlebazar.client.util.ConnectionLock;
import com.puzzlebazar.client.util.ObservableImpl;


/**
 * Keeps a record of previously executed moves.
 * Different history tracks should be used for parallel and independent types
 * of moves. For example, one track could be used for edition move and
 * another one for playing moves.  
 */
public class HistoryImpl extends ObservableImpl implements History {

	/**
	 * 
	 */
	private static final long serialVersionUID = -491997375664950660L;


	private transient boolean isSaved = true;
	private String historyInfoKey = null;
	
	private int nextMove = 0;
	private long currentTime = 0;
	private long duration = 0;
	private Vector<Move> moves = new Vector<Move>();

	private transient boolean tryToInsert = false;

	@Override
	public boolean isSaved() {
		return isSaved;
	}

	@Override
	public void setChanged() {
		super.setChanged();
		isSaved = false;
	}
	
	@Override
	public String getHistoryInfoKey() {
		return historyInfoKey;
	}

	@Override
	public void setHistoryInfoKey( String historyInfoKey ) {
		this.historyInfoKey = historyInfoKey;
	}
	
	@Override
	public HistoryMoveAddMove addMoveToHistory(Move move, PuzzleModel model) {
		if( !tryToInsert || nextMove == moves.size() ) {
			return addMoveToHistoryNoInsert( move );
		}
		// Need to insert
		// Check if the move is valid, rebuild an alternate history while doing so
		boolean failed = false;
		Vector<Move> altMoves = new Vector<Move>();
		for( int i = nextMove; i < moves.size(); ++i ) {
			try{
				Move altMove = moves.get( i ).apply( model );
				altMoves.add( altMove );
			}
			catch( Exception exception ){
				// Move cannot be inserted
				failed = true;
				Window.alert( "Inserting move here makes the puzzle invalid later. Crop first to remove all later move." );
				break;
			}
		}
		
		// Undo everything
		for( int i = altMoves.size()-1; i >= 0; --i ) {
			try {
				altMoves.get(i).unapply(model);
			}
			catch( Exception e ) {
				e.printStackTrace();
			}
		}
		
		// Check if it worked 
		if( failed ) {
			try {
				move.unapply( model );
			} catch (Exception e) {
				e.printStackTrace();
			}
			altMoves.clear();
			return null;
		}
		
		HistoryMoveAddMove result = addMoveToHistoryNoInsert( move );
		moves.addAll( altMoves );
		altMoves.clear();
		
		return result;
	}

		
	/**
	 * Add the specified move to the history, delete all moves that
	 * follow, no insertion.
	 * 
	 * @param move The move to add
	 * @return A HistoryMove that can be used for calls to update
	 */
	private HistoryMoveAddMove addMoveToHistoryNoInsert( Move move ) {
		setChanged();
		int nbMoves = moves.size();
		boolean moveDeleted = false;
		for( int i = nbMoves; i > nextMove; i-- ) {
			moveDeleted = true;
			moves.removeElementAt(i-1);
		}
		move = move.simplify();
		long time = move.getTime();
		assert( time != MoveImpl.NO_TIME );
		if( !moves.isEmpty() && moves.lastElement().getTime() == time ) {
			Move lastMove = moves.lastElement();
			if( lastMove instanceof MoveGroup ) {
				((MoveGroup)lastMove).addMove( move );
			}
			else {
				MoveGroup moveGroup = new MoveGroup(time);
				moveGroup.addMove( lastMove );
				moveGroup.addMove( move );
				moves.remove( moves.size()-1 );
				moves.add( moveGroup );
			}
		} 
		else {
			moves.add( move );
		}
		if( time > getDuration() )
			duration = time;
		nextMove = moves.size();
		currentTime = time;
		return new HistoryMoveAddMove( move, moveDeleted );
	}
	
	@Override
	public HistoryMoveDeleteMove deleteMoveFromHistory(PuzzleModel model) throws Exception {

		if( nextMove == 0 || !isOnMove() ) {
			throw new Exception( "Cannot delete, history is not currently on a move." );
		}
		
		if( !tryToInsert && nextMove != moves.size() ) {
			throw new Exception( "Cannot delete non-final move when not in insertion mode.");
		}
		
		// Unapply the move
		Move move = moves.get( nextMove-1 ).unapply(model);

		if( nextMove == moves.size() ) {
			setChanged();
			moves.remove( moves.size()-1 );
			nextMove--;
			return new HistoryMoveDeleteMove( move );
		}
		
		// Check if deletion is valid, rebuild an alternate history while doing so
		boolean failed = false;
		Vector<Move> altMoves = new Vector<Move>();
		for( int i = nextMove; i < moves.size(); ++i ) {
			try{
				Move altMove = moves.get( i ).apply( model );
				altMoves.add( altMove );
			}
			catch( Exception exception ){
				// Move cannot be deleted
				failed = true;
				break;
			}
		}
		
		// Undo everything
		for( int i = altMoves.size()-1; i >= 0; --i ) {
			try {
				altMoves.get(i).unapply(model);
			}
			catch( Exception e ) {
				e.printStackTrace();
			}
		}
		
		// Check if it worked 
		if( failed ) {
			try {
				moves.get( nextMove-1 ).apply( model );
			} catch (Exception e) {
				e.printStackTrace();
			}
			altMoves.clear();
			return null;
		}

		setChanged();
		for( int i = moves.size(); i > nextMove-1; i-- ) {
			moves.removeElementAt(i-1);
		}
		nextMove--;
		moves.addAll( altMoves );
		altMoves.clear();
		
		return  new HistoryMoveDeleteMove( move );
	}

	@Override
	public HistoryMoveChangeTime undo( PuzzleModel model ) throws Exception {
		if( nextMove == 0 ) return new HistoryMoveChangeTime( new MoveEmpty(MoveImpl.NO_TIME) );
		setChanged();
		nextMove--;
		if( nextMove == 0 ) 
			currentTime = 0;
		else
			currentTime = moves.get( nextMove-1 ).getTime();
		return new HistoryMoveChangeTime( moves.get( nextMove ).unapply( model ) );
	}
	
	@Override
	public HistoryMoveChangeTime redo( PuzzleModel model ) throws Exception {
		if( nextMove >= moves.size() ) return new HistoryMoveChangeTime( new MoveEmpty(MoveImpl.NO_TIME) );
		setChanged();
		Move result = moves.get( nextMove ).apply( model );
		nextMove++;
		currentTime = moves.get( nextMove-1 ).getTime();
		return new HistoryMoveChangeTime( result );
	}

	@Override
	public HistoryMoveChangeTime undoAll(PuzzleModel puzzle) throws Exception {
		MoveList result = new MoveList(0);
		while( canUndo() ) {
			result.append( undo(puzzle).getPuzzleMove() );
		}
		return new HistoryMoveChangeTime( result );
	}
	
	@Override
	public HistoryMoveChangeTime redoAll(PuzzleModel puzzle) throws Exception {
		MoveList result = new MoveList( getDuration() );
		while( canRedo() ) {
			result.append( redo(puzzle).getPuzzleMove() );
		}
		return new HistoryMoveChangeTime( result );
	}

	@Override
	public HistoryMoveChangeTime snapToPreviousMove() {
		if( nextMove == 0 ) return new HistoryMoveChangeTime( new MoveEmpty(MoveImpl.NO_TIME) );
		setChanged();
		currentTime = moves.get( nextMove-1 ).getTime();
		return new HistoryMoveChangeTime( new MoveList( currentTime ) );
	}
	@Override
	public HistoryMoveChangeTime setTime(long time, PuzzleModel puzzle) throws Exception {
		if( time < 0 ) 
			time = 0;
		if( time > getDuration() ) 
			time = getDuration();
	
		MoveList result = new MoveList( time );
		if( time < currentTime ) {
			while( canUndo() && moves.get(nextMove-1).getTime() > time )
				result.append( undo(puzzle).getPuzzleMove() );
			currentTime = time;
			setChanged();
		} else {
			while( canRedo() && moves.get(nextMove).getTime() <= time )
				result.append( redo(puzzle).getPuzzleMove() );
			currentTime = time;
			setChanged();
		}
	
		return new HistoryMoveChangeTime( result );
	}
	
	@Override
	public void applyMovesUpToCurrentTime( PuzzleModel model ) throws Exception {
		for( int i=0; i < nextMove; ++i )
			moves.get(i).apply( model );
	}

	@Override
	public void moveTimeZeroIntoModel(PuzzleModel puzzle) throws Exception {
		long previousTime = currentTime;
		setTime( 0, puzzle );
		while( !moves.isEmpty() ) {
			if( moves.get(0).getTime() != 0 )
				break;
			moves.remove(0);
			nextMove--;
		}
		setTime( previousTime, puzzle );
	}

	@Override
	public void forceCurrentMoveToZero() {
		nextMove = 0;
		currentTime = 0;
		setChanged();
	}
	
	@Override
	public boolean canUndo() {		
		return nextMove > 0;
	}

	@Override
	public boolean canRedo() {	
		return nextMove < moves.size();
	}
	
	@Override
	public HistoryMoveClear clear( Move puzzleMove ) {
		setChanged();
		moves.clear();
		nextMove = 0;

		return new HistoryMoveClear( puzzleMove );
	}

	@Override
	public Vector<Move> getMoves() {
		return moves;
	}

		
	@Override
	public int getCurrentMoveIndex() {
		return nextMove-1;
	}

	@Override
	public Move getCurrentMove() {
		if( nextMove == 0 )
			return null;
		return moves.get(nextMove-1);
	}


	@Override
	public long getLastMoveTime() {
		if( moves.isEmpty() )
			return 0;
		return moves.lastElement().getTime();
	}

	@Override
	public long getDuration() {
		return duration;
	}

	@Override
	public long getCurrentTime() {
		return currentTime;
	}

	@Override
	public MoveSaveHistory saveHistory(ConnectionLock lock, String puzzleInfoKey, boolean completed) {

		assert( puzzleInfoKey != null );
		
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.saveHistory( 
				this,
				puzzleInfoKey,
				completed,
				new AsyncCallbackLock<String>(lock){
					@Override
					public void onFailure(Throwable caught) {
						super.onFailure(caught);
						setChanged();
						isSaved = false;
						notifyObservers( new MoveSaveHistoryFailed( MoveImpl.NO_TIME ) );
						Window.alert( "Save history failed!" );
					}
					@Override
					public void onSuccess(String result) {
						super.onSuccess(result);
						historyInfoKey = result;
					} });
	
		setChanged();
		isSaved = true;
		return new MoveSaveHistory( MoveImpl.NO_TIME );
	}


	@Override
	public MoveSaveTutorial saveTutorial(ConnectionLock lock, PuzzleModel puzzleModel ) {

		assert( puzzleModel != null );
		
		PuzzleServiceAsync puzzleService = ServiceFactory.getPuzzleService();
		puzzleService.saveTutorial( 
				this,
				puzzleModel,
				new AsyncCallbackLock<String>(lock){
					@Override
					public void onFailure(Throwable caught) {
						super.onFailure(caught);
						setChanged();
						isSaved = false;
						notifyObservers( new MoveSaveTutorialFailed( MoveImpl.NO_TIME ) );
						Window.alert( "Save tutorial failed!" );
					}
					@Override
					public void onSuccess(String result) {
						super.onSuccess(result);
						historyInfoKey = result;
					} });
	
		setChanged();
		isSaved = true;
		return new MoveSaveTutorial( MoveImpl.NO_TIME );
	}

	@Override
	public MoveChangeDuration changeDuration(long duration) throws Exception {
		if( !moves.isEmpty() && duration < moves.lastElement().getTime() )
			throw new Exception( "Invalid duration" );
		this.duration = duration;
		setChanged();
		return new MoveChangeDuration( duration );
	}

	@Override
	public MoveChangeDuration crop() {
		if( currentTime != duration ) {		
			setChanged();
			int nbMoves = moves.size();
			for( int i = nbMoves; i > nextMove; i-- ) {
				moves.removeElementAt(i-1);
			}
			duration = currentTime;
		}
		
		return new MoveChangeDuration( duration );
		
	}
	
	@Override
	public MoveChangeDuration retime(long time) {
		for (Move move : moves) {
			move.setTime( move.getTime() * time / duration );
		}		
		currentTime = currentTime * time / duration;
		duration = time;
		
		return new MoveChangeDuration( duration );
	}

	@Override
	public int getMoveIndex(Move move) {
		int index = 0;
		for (Move historyMove : moves) {
			if( historyMove == move )
				break;
			++index;
		}
		if( index == moves.size() )
			return -1;
		return index;
	}

	@Override
	public MoveRetimeMove retimeMove(Move move, long time)  throws Exception {
		int index = getMoveIndex( move );
		
		if( index < 0 )
			throw new Exception( "Retiming invalid move");
		
		setChanged();
		
		long minTime = getMinTime(index);
		long maxTime = getMaxTime(index);			
		if( time < minTime )
			time = minTime;
		if( maxTime != MoveImpl.NO_TIME && time > maxTime )
			time = maxTime;
		
		long oldTime = move.getTime();
		
		if( currentTime > oldTime ) {			
			if( maxTime == MoveImpl.NO_TIME ) {
				if( currentTime < time )
					currentTime = time;
			}
			else {
				if( currentTime < maxTime )
					currentTime = (long)(time + (float)(currentTime - oldTime) / (maxTime - oldTime) * (maxTime - time)); 
			}
		}
		else {			
			if( currentTime > minTime )
				currentTime = (long)(minTime + (float)(currentTime - minTime) / (oldTime - minTime) * (time - minTime)); 
		}
		
		move.setTime( time );
		
		if( time > duration )
			duration = time;
		
		return new MoveRetimeMove( MoveImpl.NO_TIME );
	}

	@Override
	public MoveRetimeMove retimeMoveAndFollowing(Move move, long time)  throws Exception {
		int index = getMoveIndex( move );
		
		if( index < 0 )
			throw new Exception( "Retiming invalid move");
		
		setChanged();
		
		long minTime = getMinTime(index);

		if( time < minTime )
			time = minTime;
		
		long oldTime = move.getTime();
		long timeDiff = time - oldTime;
		
		if( currentTime >= oldTime ) {
			currentTime = currentTime + timeDiff;
		}
		else {			
			if( currentTime > minTime )
				currentTime = (long)(minTime + (float)(currentTime - minTime) / (oldTime - minTime) * (time - minTime)); 
		}
		
		for( int i = moves.size()-1; i>=index; --i ) {
			Move currMove = moves.get(i);
			currMove.setTime( currMove.getTime() + timeDiff );
		}
		
		duration += timeDiff;
		
		return new MoveRetimeMove( MoveImpl.NO_TIME );
	}

	@Override
	public MoveRetimeMove explodeMove(Move move) throws Exception {
		
		if( !(move instanceof MoveGroup) )
			return null;

		int index = getMoveIndex( move );
		
		if( index < 0 )
			throw new Exception( "Retiming invalid move");
		
		setChanged();

		long minTime = getMinTime(index);
		long maxTime = getMaxTime(index);
		if( maxTime == MoveImpl.NO_TIME )
			maxTime = getDuration();
		

		MoveGroup moveGroup = (MoveGroup)move;

		moves.remove(index);
		
		int size = moveGroup.getMoveCount();
		float ratio = (maxTime-minTime) / (float)(size+1);
		for( int i = 0; i < size; i++ ) {
			long time = minTime + (long)((i+1)*ratio);
			Move subMove = moveGroup.getMove(i);
			subMove.setTime( time );
			moves.insertElementAt( subMove, index+i );
		}
		
		if( nextMove > index )
			nextMove += size-1;
		
		long oldTime = moveGroup.getTime();

		if( currentTime >= oldTime ) {			
			if( currentTime < maxTime ) {
				long lastSubTime = minTime + (long)(size*ratio);
				currentTime = (long)(lastSubTime + (float)(currentTime - oldTime) / (maxTime - oldTime) * (maxTime - lastSubTime)); 
			}
		}
		else {			
			if( currentTime > minTime ) {
				long firstSubTime = minTime + (long)ratio;
				currentTime = (long)(minTime + (float)(currentTime - minTime) / (oldTime - minTime) * (firstSubTime - minTime)); 
			}
		}
		
		return new MoveRetimeMove( MoveImpl.NO_TIME );
	}

	/**
	 * Get the minimum time to which this move can be moved (time of previous move or 0)
	 * 
	 * @param index The index of the move to query
	 * @return The minimum time
	 */
	private long getMinTime(int index) {
		if( index > 0 )
			return moves.get(index-1).getTime();
		return 0;
	}

	/**
	 * Get the maximum time to which this move can be moved (time of next move or NO_TIME)
	 * 
	 * @param index The index of the move to query
	 * @return The maximum time
	 */
	private long getMaxTime(int index) {
		if( index < moves.size()-1 )
			return moves.get(index+1).getTime();
		return MoveImpl.NO_TIME;
	}
	
	@Override
	public HistoryMoveChangeNodes mergeOverlappingMoves(PuzzleModel puzzleModel) {

		HistoryMoveChangeNodes result;
		try {
			// A side effect of this call is that if the current time is directly on a move
			// that has not yet been applied to the model, then this move will be applied
			HistoryMove move = setTime( currentTime, puzzleModel );
			result = new HistoryMoveChangeNodes( move.getPuzzleMove() );
		} catch (Exception e) {
			e.printStackTrace();
			result = new HistoryMoveChangeNodes( new MoveList( currentTime ) );
		}
		
		for( int index = 0; index < moves.size()-1; ++index ) {
			long moveTime = moves.get(index).getTime();
			int nbMovesAtSameTime = 1;
			while( index+nbMovesAtSameTime < moves.size() && 
				   moves.get(index+nbMovesAtSameTime).getTime() == moveTime )
				nbMovesAtSameTime++;
			if( nbMovesAtSameTime > 1 ) {
				setChanged();
				MoveGroup moveGroup = new MoveGroup(moveTime);
				for( ; nbMovesAtSameTime > 1;  --nbMovesAtSameTime ) {
					moveGroup.addMove( moves.get(index) );
					moves.remove(index);
					if( nextMove > index )
						--nextMove;
				}
				moveGroup.addMove( moves.get(index) );
				moves.set(index, moveGroup);
			}
			
		}
		
		return result;
	}


	@Override
	public boolean isOnMove() {
		return nextMove > 0 && (currentTime == moves.get(nextMove-1).getTime() );
	}

	@Override
	public boolean isOnMessageMove() {
		if( !isOnMove() )
			return false;
		
		MoveMessageInfo moveBefore = findPreviousMoveMessage( nextMove-1 );
		return moveBefore != null && moveBefore.moveIndex == (nextMove-1);
	}
	
	@Override
	public boolean isOnExplodableMove() {
		if( !isOnMove() )
			return false;		

		return (moves.get(nextMove-1) instanceof MoveGroup);
	}

	@Override
	public String getMessageAtMove(int moveIndex) {

		if( moveIndex >= moves.size() )
			moveIndex = moves.size()-1;
		
		MoveMessageInfo moveInfo = findPreviousMoveMessage(moveIndex);
		if( moveInfo == null )
			return null;
		return moveInfo.moveMessage.getMessage();
	}

	
	@Override
	public HistoryMoveChangeMessage changeMessageAtMove(int moveIndex, String message, PuzzleModel puzzleModel ) throws Exception {

		if( moveIndex >= moves.size() )
			moveIndex = moves.size()-1;
		
		MoveMessageInfo moveBefore = findPreviousMoveMessage(moveIndex);
		MoveMessageInfo moveAfter = findNextMoveMessage(moveIndex);
		if( moveBefore == null )
			throw new Exception("Unable to change the message, no MoveMessage prior the the specified index.");
		moveBefore.moveMessage.setMessage( message );
		if( moveAfter != null )
			moveAfter.moveMessage.setPreviousMessage( message );
		
		if( nextMove > moveBefore.moveIndex ) {
			if( moveAfter == null || nextMove <= moveAfter.moveIndex )
				return new HistoryMoveChangeMessage( moveBefore.moveMessage.apply( puzzleModel ) );
		}
		
		return null;
	}


	private class MoveMessageInfo {
		public int moveIndex = -1;
		public MoveMessage moveMessage = null;
	}
	
	/**
	 * Look for the first MoveMessage that comes exactly at or before the specified move index
	 * 
	 * @param moveIndex The index where to start the backwards search
	 * @return The information on MoveMessage found
	 */
	private MoveMessageInfo findPreviousMoveMessage(int moveIndex) {
		if( moveIndex < 0 || moves.size() == 0 )
			return null;

		MoveMessageInfo result = new MoveMessageInfo();
		
		for( int i = moveIndex; i >= 0; --i ) {
			Move move = moves.get(i);
			if( move instanceof MoveMessage ) {
				result.moveIndex = i;
				result.moveMessage = (MoveMessage)move;
				return result;
			}
			if( move instanceof MoveGroup ) {
				MoveGroup moveGroup = (MoveGroup)move;
				for( int j=moveGroup.getMoveCount()-1; j >= 0; --j ) {
					move = moveGroup.getMove( j );
					if( move instanceof MoveMessage ) {
						result.moveIndex = i;
						result.moveMessage = (MoveMessage)move;
						return result;
					}
				}
			}
		}
		
		return null;
	}



	/**
	 * Look for the first MoveMessage that comes after the specified move index
	 * 
	 * @param moveIndex The index where to start the forward search
	 * @param nextIndex The index at which the message move was found
	 * @return The information on MoveMessage found
	 */
	private MoveMessageInfo findNextMoveMessage(int moveIndex) {
		if( moveIndex < 0 || moves.size() == 0 )
			return null;

		MoveMessageInfo result = new MoveMessageInfo();
		
		for( int i = moveIndex+1; i < moves.size(); ++i ) {
			Move move = moves.get(i);
			if( move instanceof MoveMessage ) {
				result.moveIndex = i;
				result.moveMessage = (MoveMessage)move;
				return result;
			}
			if( move instanceof MoveGroup ) {
				MoveGroup moveGroup = (MoveGroup)move;
				for( int j=0; j < moveGroup.getMoveCount(); ++j ) {
					move = moveGroup.getMove( j );
					if( move instanceof MoveMessage ) {
						result.moveIndex = i;
						result.moveMessage = (MoveMessage)move;
						return result;
					}
				}
			}
		}
		
		return null;
	}

	@Override
	public void setInsert(boolean tryToInsert) {
		this.tryToInsert = tryToInsert;
	}


}
